/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text.method;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ReplacementSpan;

import junit.framework.Assert;

/**
 * Represents an editor state.
 *
 * The editor state can be specified by following string format.
 * - Components are separated by space(U+0020).
 * - Single-quoted string for printable ASCII characters, e.g. 'a', '123'.
 * - U+XXXX form can be used for a Unicode code point.
 * - Components inside '[' and ']' are in selection.
 * - Components inside '(' and ')' are in ReplacementSpan.
 * - '|' is for specifying cursor position.
 *
 * Selection and cursor can not be specified at the same time.
 *
 * Example:
 *   - "'Hello,' | U+0020 'world!'" means "Hello, world!" is displayed and the cursor position
 *     is 6.
 *   - "'abc' [ 'def' ] 'ghi'" means "abcdefghi" is displayed and "def" is selected.
 *   - "U+1F441 | ( U+1F441 U+1F441 )" means three U+1F441 characters are displayed and
 *     ReplacementSpan is set from offset 2 to 6.
 */
public class EditorState {
    private static final String REPLACEMENT_SPAN_START = "(";
    private static final String REPLACEMENT_SPAN_END = ")";
    private static final String SELECTION_START = "[";
    private static final String SELECTION_END = "]";
    private static final String CURSOR = "|";

    public Editable mText;
    public int mSelectionStart = -1;
    public int mSelectionEnd = -1;

    public EditorState() {
    }

    // Returns true if the code point is ASCII and graph.
    private boolean isGraphicAscii(int codePoint) {
        return 0x20 < codePoint && codePoint < 0x7F;
    }

    // Setup editor state with string. Please see class description for string format.
    public void setByString(String string) {
        final StringBuilder sb = new StringBuilder();
        int replacementSpanStart = -1;
        int replacementSpanEnd = -1;
        mSelectionStart = -1;
        mSelectionEnd = -1;

        final String[] tokens = string.split(" +");
        for (String token : tokens) {
            if (token.startsWith("'") && token.endsWith("'")) {
                for (int i = 1; i < token.length() - 1; ++i) {
                    final char ch = token.charAt(1);
                    if (!isGraphicAscii(ch)) {
                        throw new IllegalArgumentException(
                                "Only printable characters can be in single quote. " +
                                "Use U+" + Integer.toHexString(ch).toUpperCase() + " instead");
                    }
                }
                sb.append(token.substring(1, token.length() - 1));
            } else if (token.startsWith("U+")) {
                final int codePoint = Integer.parseInt(token.substring(2), 16);
                if (codePoint < 0 || 0x10FFFF < codePoint) {
                    throw new IllegalArgumentException("Invalid code point is specified:" + token);
                }
                sb.append(Character.toChars(codePoint));
            } else if (token.equals(CURSOR)) {
                if (mSelectionStart != -1 || mSelectionEnd != -1) {
                    throw new IllegalArgumentException(
                            "Two or more cursor/selection positions are specified.");
                }
                mSelectionStart = mSelectionEnd = sb.length();
            } else if (token.equals(SELECTION_START)) {
                if (mSelectionStart != -1) {
                    throw new IllegalArgumentException(
                            "Two or more cursor/selection positions are specified.");
                }
                mSelectionStart = sb.length();
            } else if (token.equals(SELECTION_END)) {
                if (mSelectionEnd != -1) {
                    throw new IllegalArgumentException(
                            "Two or more cursor/selection positions are specified.");
                }
                mSelectionEnd = sb.length();
            } else if (token.equals(REPLACEMENT_SPAN_START)) {
                if (replacementSpanStart != -1) {
                    throw new IllegalArgumentException(
                            "Only one replacement span is supported");
                }
                replacementSpanStart = sb.length();
            } else if (token.equals(REPLACEMENT_SPAN_END)) {
                if (replacementSpanEnd != -1) {
                    throw new IllegalArgumentException(
                            "Only one replacement span is supported");
                }
                replacementSpanEnd = sb.length();
            } else {
                throw new IllegalArgumentException("Unknown or invalid token: " + token);
            }
        }

        if (mSelectionStart == -1 || mSelectionEnd == -1) {
              if (mSelectionEnd != -1) {
                  throw new IllegalArgumentException(
                          "Selection start position doesn't exist.");
              } else if (mSelectionStart != -1) {
                  throw new IllegalArgumentException(
                          "Selection end position doesn't exist.");
              } else {
                  throw new IllegalArgumentException(
                          "At least cursor position or selection range must be specified.");
              }
        } else if (mSelectionStart > mSelectionEnd) {
              throw new IllegalArgumentException(
                      "Selection start position appears after end position.");
        }

        final Spannable spannable = new SpannableString(sb.toString());

        if (replacementSpanStart != -1 || replacementSpanEnd != -1) {
            if (replacementSpanStart == -1) {
                throw new IllegalArgumentException(
                        "ReplacementSpan start position doesn't exist.");
            }
            if (replacementSpanEnd == -1) {
                throw new IllegalArgumentException(
                        "ReplacementSpan end position doesn't exist.");
            }
            if (replacementSpanStart > replacementSpanEnd) {
                throw new IllegalArgumentException(
                        "ReplacementSpan start position appears after end position.");
            }

            ReplacementSpan mockReplacementSpan = mock(ReplacementSpan.class);
            when(mockReplacementSpan.getSize(any(), any(), any(), any(), any()))
                .thenReturn(0);
            doNothing().when(mockReplacementSpan)
                .draw(any(), any(), any(), any(), any(), any(), any(), any(), any());

            spannable.setSpan(mockReplacementSpan, replacementSpanStart, replacementSpanEnd,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        mText = Editable.Factory.getInstance().newEditable(spannable);
    }

    public void assertEquals(String string) {
        EditorState expected = new EditorState();
        expected.setByString(string);

        Assert.assertEquals(expected.mText.toString(), mText.toString());
        Assert.assertEquals(expected.mSelectionStart, mSelectionStart);
        Assert.assertEquals(expected.mSelectionEnd, mSelectionEnd);
    }
}

